package cn.sciento.core.interceptor;

import cn.sciento.core.exception.NotFoundException;
import cn.sciento.core.oauth.CustomUserDetails;
import cn.sciento.core.oauth.DetailsHelper;
import cn.sciento.core.util.AsyncTask;
import cn.sciento.core.util.CommonExecutor;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.stream.Collectors;

public final class InterceptorChainBuilder<T> {
  private final ConcurrentHashMap<ChainId, List<Class<? extends HandlerInterceptor<T>>>> preInterMap = new ConcurrentHashMap<>();
  
  private final ConcurrentHashMap<ChainId, List<Class<? extends HandlerInterceptor<T>>>> postInterMap = new ConcurrentHashMap<>();
  
  private final ConcurrentHashMap<ChainId, Boolean> preAsyncMap = new ConcurrentHashMap<>();
  
  private final ConcurrentHashMap<ChainId, Boolean> postAsyncMap = new ConcurrentHashMap<>();
  
  private ChainId currentChainId;
  
  private boolean post = true;
  
  private final Set<ChainId> chainIds = new HashSet<>();
  
  private static final String PRE = "Pre-";
  
  private static final String POST_PRE = "Post-";
  
  private final List<HandlerInterceptor<T>> interceptors;
  
  public InterceptorChainBuilder(List<HandlerInterceptor<T>> interceptors) {
    this.interceptors = interceptors;
  }
  
  public InterceptorChainBuilder<T> selectChain(ChainId chainId) {
    this.currentChainId = chainId;
    this.chainIds.add(chainId);
    this.preInterMap.putIfAbsent(this.currentChainId, new ArrayList<>(8));
    this.postInterMap.putIfAbsent(this.currentChainId, new ArrayList<>(8));
    this.preAsyncMap.putIfAbsent(this.currentChainId, Boolean.FALSE);
    this.postAsyncMap.putIfAbsent(this.currentChainId, Boolean.FALSE);
    return this;
  }
  
  protected void clearChain() {
    this.currentChainId = null;
  }
  
  public InterceptorChainBuilder<T> pre() {
    this.post = false;
    return this;
  }
  
  public InterceptorChainBuilder<T> post() {
    this.post = true;
    return this;
  }
  
  public InterceptorChainBuilder<T> async() {
    checkState();
    if (this.post) {
      this.postAsyncMap.put(this.currentChainId, Boolean.TRUE);
    } else {
      this.preAsyncMap.put(this.currentChainId, Boolean.TRUE);
    } 
    return this;
  }
  
  public InterceptorChainBuilder<T> sync() {
    checkState();
    if (this.post) {
      this.postAsyncMap.put(this.currentChainId, Boolean.FALSE);
    } else {
      this.preAsyncMap.put(this.currentChainId, Boolean.FALSE);
    } 
    return this;
  }
  
  public InterceptorChainBuilder<T> addInterceptorAfter(Class<? extends HandlerInterceptor<T>> interceptor, Class<? extends HandlerInterceptor<T>> afterInterceptor) {
    checkState();
    int index = checkInterceptor(afterInterceptor);
    addAt(index + 1, interceptor);
    return this;
  }
  
  public InterceptorChainBuilder<T> addInterceptorBefore(Class<? extends HandlerInterceptor<T>> interceptor, Class<? extends HandlerInterceptor<T>> beforeInterceptor) {
    checkState();
    int index = checkInterceptor(beforeInterceptor);
    addAt(index - 1, interceptor);
    return this;
  }
  
  public InterceptorChainBuilder<T> addInterceptor(Class<? extends HandlerInterceptor<T>> interceptor) {
    checkState();
    addLast(interceptor);
    return this;
  }
  
  public InterceptorChainBuilder<T> addInterceptorAt(Class<? extends HandlerInterceptor<T>> interceptor, Class<? extends HandlerInterceptor<T>> atInterceptor) {
    checkState();
    int index = checkInterceptor(atInterceptor);
    getCurrentInterceptors().remove(index);
    addAt(index, interceptor);
    return this;
  }
  
  public InterceptorChainBuilder<T> removeInterceptor(Class<? extends HandlerInterceptor<T>> interceptor) {
    checkState();
    int index = checkInterceptor(interceptor);
    getCurrentInterceptors().remove(index);
    return this;
  }
  
  private void checkState() {
    if (this.currentChainId == null)
      throw new UnsupportedOperationException("Please call selectChain First."); 
  }
  
  private int checkInterceptor(Class<? extends HandlerInterceptor<T>> interceptor) {
    List<Class<? extends HandlerInterceptor<T>>> interceptors = getCurrentInterceptors();
    for (int i = 0; i < interceptors.size(); i++) {
      if (interceptor.isAssignableFrom(interceptors.get(i)))
        return i; 
    } 
    throw new NotFoundException("HandlerInterceptor not found for [" + interceptor.getName() + "]");
  }
  
  private void addAt(int index, Class<? extends HandlerInterceptor<T>> interceptor) {
    List<Class<? extends HandlerInterceptor<T>>> interceptors = getCurrentInterceptors();
    interceptors.add(Math.max(index, 0), interceptor);
  }
  
  private void addLast(Class<? extends HandlerInterceptor<T>> interceptor) {
    List<Class<? extends HandlerInterceptor<T>>> interceptors = getCurrentInterceptors();
    interceptors.add(interceptor);
  }
  
  private List<Class<? extends HandlerInterceptor<T>>> getCurrentInterceptors() {
    return this.post ? this.postInterMap.get(this.currentChainId) : this.preInterMap.get(this.currentChainId);
  }
  
  public List<InterceptorChain<T>> performBuild() {
    List<InterceptorChain<T>> chains = new ArrayList<>();
    for (Iterator<ChainId> iterator = this.chainIds.iterator(); iterator.hasNext(); ) {
      ChainId chainId = iterator.next();
      List<HandlerInterceptor<T>> preInterceptors = filterInterceptor(this.preInterMap.get(chainId));
      List<HandlerInterceptor<T>> postInterceptors = filterInterceptor(this.postInterMap.get(chainId));
      if (preInterceptors.isEmpty() && postInterceptors.isEmpty())
        throw new IllegalStateException("Registered Pre-Interceptors and Post-Interceptors is empty."); 
      Consumer<T> preConsumer = t -> {
        
        };
      Consumer<T> postConsumer = t -> {
        
        };
      if (!preInterceptors.isEmpty())
        if (((Boolean)this.preAsyncMap.get(chainId)).booleanValue()) {
          preConsumer = (obj -> {
              List<AsyncTask<T>> tasks = createAsyncTask(preInterceptors, (T)obj);
              CommonExecutor.batchExecuteAsync(tasks, "Pre-" + chainId.id());
            });
        } else {
          preConsumer = (obj -> {
              for (HandlerInterceptor<T> item : (Iterable<HandlerInterceptor<T>>)preInterceptors)
                item.interceptor((T)obj); 
            });
        }  
      if (!postInterceptors.isEmpty())
        if (((Boolean)this.postAsyncMap.get(chainId)).booleanValue()) {
          postConsumer = (obj -> {
              List<AsyncTask<T>> tasks = createAsyncTask(postInterceptors, (T)obj);
              CommonExecutor.batchExecuteAsync(tasks, "Post-" + chainId.id());
            });
        } else {
          postConsumer = (obj -> {
              for (HandlerInterceptor<T> item : (Iterable<HandlerInterceptor<T>>)postInterceptors)
                item.interceptor((T)obj); 
            });
        }  
      chains.add(new InterceptorChain<>(chainId, preConsumer, postConsumer));
    } 
    return chains;
  }
  
  private List<HandlerInterceptor<T>> filterInterceptor(List<Class<? extends HandlerInterceptor<T>>> classList) {
    return (List<HandlerInterceptor<T>>)classList.stream()
      .map(clazz -> {
          for (HandlerInterceptor<T> interceptor : this.interceptors) {
            if (clazz.getName().equals(interceptor.getClass().getName()))
              return interceptor; 
          } 
          throw new NotFoundException("HandlerInterceptor Bean not found for [" + clazz.getName() + "]");
        }).collect(Collectors.toList());
  }
  
  private List<AsyncTask<T>> createAsyncTask(List<HandlerInterceptor<T>> interceptors, final T target) {
    final CustomUserDetails self = DetailsHelper.getUserDetails();
    return interceptors.stream().map(item -> new AsyncTask<T>() {
          public String taskName() {
            return item.getClass().getSimpleName();
          }
          
          public T doExecute() {
            if (self != null)
              DetailsHelper.setCustomUserDetails(self); 
            item.interceptor(target);
            return (T)target;
          }
        }).collect(Collectors.toList());
  }
}
